import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;


/**
 * The class TrackerInterface contains all of the methods necessary to access
 * the tracker, communicate client status updates to the tracker, and update
 * the local list of peers.
 * 
 * @author Matthew Pies
 *
 */
public class TrackerInterface {

    /**
     * This is the string that prefaces the peerID during tracker communication.
     */
    public static final String clientID = "-gM2000-";

    /**
     * The port that the client is communicating on, typically 6881.
     */
    public static final int clientPort = 54321;
    public static final int maxInterval = 180; // in seconds

    private static Bencoder bencode = new Bencoder ();

    private String peerID;
    private String trackerURL;
    private int interval;     // in seconds
    private Vector<PeerInfo> peers;
    private AtomicLong uploadedBytes;
    private AtomicLong downloadedBytes;
    private AtomicLong bytesLeft;
    private String failureReason;
    private BufferedInputStream in;
    private AtomicBoolean started;
    private Thread updateThread;
    private GlobalTorrentInfo info;

    /**
     * Constructs the basic tracker interfacing elements.
     * 
     * Preconditions: "torrent" must not be null
     * Postconditions: "started()" must be called before any other methods
     *      that interface with the tracker are used
     * 
     * @param torrent - the TorrentFile representation of the *.torrent file
     */
    public TrackerInterface (TorrentFile torrent, GlobalTorrentInfo info) {
        StringBuilder tmp = new StringBuilder (clientID);
        for (int k=0; k<20-clientID.length(); k++)
            tmp.append((int) (10 * Math.random()));
        peerID = tmp.toString();

        StringBuilder tmpURL = new StringBuilder (torrent.tracker_url);
        tmpURL.append("?info_hash=");
        tmpURL.append(torrent.info_hash_as_url);
        tmpURL.append("&peer_id=");
        tmpURL.append(peerID);
        tmpURL.append("&port=" + clientPort);
        tmpURL.append("&compact=1");
        trackerURL = new String (tmpURL);

        peers = new Vector <PeerInfo> ();
        uploadedBytes = new AtomicLong (0);
        downloadedBytes = new AtomicLong (0);
        bytesLeft = new AtomicLong (torrent.file_length);
        interval = maxInterval;
        failureReason = null;
        in = null;
        started = new AtomicBoolean(false);
        updateThread = null;

        this.info = info;
        this.info.tracker = this;
    }

    @SuppressWarnings("unchecked")
    private boolean updateTracker (String url, boolean returnInfo) {
        StringBuilder tmp = new StringBuilder (url);
        tmp.append("&uploaded=");
        tmp.append(uploadedBytes.get());
        tmp.append("&downloaded=");
        tmp.append(downloadedBytes.get());
        if (bytesLeft.get() > 0) {
            tmp.append("&left=");
            tmp.append(bytesLeft.get());
        }

        URL trackerURL;
        try {
            trackerURL = new URL (tmp.toString());
            System.out.println(trackerURL.toString());
        } catch (MalformedURLException e) {
            System.err.println("Malformed tracker URL: " + tmp.toString());
            System.err.println("Tracker information not updated");
            return false;
        }

        if (!returnInfo) {
            try {
                trackerURL.openConnection().connect();
            } catch (IOException e) {
                System.err.println("Connection error with tracker");
            }
            return true;
        }

        HashMap<String,Object> trackerMap = null;
        try {
            in = getStream(trackerURL);
            if (in == null)
                return false;
            ByteArrayOutputStream bytes = new ByteArrayOutputStream ();
            int c;
            while ((c = in.read()) != -1) {
                bytes.write(c);
            }
            in.close();
            trackerMap = bencode.unbencodeDictionary(bytes.toByteArray());
        } catch (IOException e) {
            System.err.println("Problem communicating with tracker");
            System.err.println("Tracker information not updated");
            return false;
        }

        failureReason = (String) trackerMap.get("failure reason");
        if (failureReason != null) {
            System.err.println("Tracker failure: " + failureReason);
            return false;
        }

        Object tmpInterval = (Integer) trackerMap.get("interval");
        if (tmpInterval != null && (Integer) tmpInterval < maxInterval)
            interval = (Integer) tmpInterval;

        Object benPeers = trackerMap.get("peers");
        synchronized (peers) {
            peers.clear();
            if (benPeers instanceof String) {
                String tmp2 = (String) benPeers;
                for (int k=0; k<=tmp2.length()-6; k+=6) {
                    peers.add(PeerInfo.createPeerInfo(tmp2.substring(k,k+6)));
                }
            } else {
                Vector vec = (Vector) benPeers;
                for (int k=0; k<vec.size(); k++) {
                    peers.add(PeerInfo.createPeerInfo(vec.get(k)));
                }
            }
        }
        return true;
    }

    private static BufferedInputStream getStream (URL url) throws IOException {
        boolean finished = false;
        BufferedInputStream rtn = null;
        while (!finished && !Thread.currentThread().isInterrupted()) {
            try {
                URLConnection conn = url.openConnection();
                conn.setReadTimeout(5000);
                conn.setConnectTimeout(5000);
                rtn = new BufferedInputStream (conn.getInputStream());
                finished = true;
            } catch (SocketTimeoutException e) {
                System.out.println("Tracker connection timed out, retrying");
            }
        }
        return rtn;
    }

    private boolean updateTracker () {
        return updateTracker (trackerURL, true);
    }

    @SuppressWarnings("unchecked")
    public Vector<PeerInfo> returnPeers() {
        return (Vector<PeerInfo>) peers.clone();
    }

    public void stopTracker () {
        if (!started.get())
            throw new IllegalStateException ("Tracker cannot be stopped");
        if (updateThread != null)
            updateThread.interrupt();
        try {
            if (in != null)
                in.close();
        } catch (IOException e) {}
        updateThread = null;
        started.set(false);
    }

    public void startTracker () {
        if (!started.compareAndSet(false, true))
            throw new IllegalStateException ("Tracker already started");
        updateThread = new Thread (new TrackerUpdate ());
        updateThread.start();
    }

    public boolean completed () {
        if (started.get())
            return updateTracker (trackerURL + "&event=completed", false);
        throw new IllegalStateException ("Cannot 'complete' tracker w/o starting");
    }

    public void increaseUploadedBy (long amt) {
        uploadedBytes.addAndGet(amt);
    }

    public void increaseDownloadedBy (long amt) {
        downloadedBytes.addAndGet(amt);
    }
    
    public float ratio () {
        return ((float) uploadedBytes.get()) / downloadedBytes.get();
    }

    public void decreaseBytesLeftBy (long amt) {
        bytesLeft.set(bytesLeft.get() - amt);
    }

    public byte[] getPeerIdBytes() throws UnsupportedEncodingException {
        return peerID.getBytes("US-ASCII");
    }

    private class TrackerUpdate implements Runnable {
        public void run() {
            try {
                while (!updateTracker(trackerURL + "&event=started", true)) {
                    System.out.println("Failed to start tracker connection, retrying");
                    Thread.sleep(5000);
                }
            } catch (InterruptedException e) {
                return;
            }
            System.out.println("Tracker connection started");
            System.out.println("Peers: " + peers);
            System.out.println("Interval: " + interval);

            try {
                while (!Thread.currentThread().isInterrupted()) {
                    Thread.sleep(interval * 1000);
                    updateTracker();
                    System.out.println("tracker found " + peers.size() +" peers");
                }
            } catch (InterruptedException e) {
                updateTracker(trackerURL + "&event=stopped", false);
                System.out.println("Tracker connection stopped");
            }
        }
    }
}
